// -------------------------------------------------
// BlinKit - BlinKit Library
// -------------------------------------------------
//   File Name: script_wrappable_impl.cpp
// Description: ScriptWrappable Class
//      Author: Ziming Li
//     Created: 2022-03-02
// -------------------------------------------------
// Copyright (C) 2022 MingYang Software Technology.
// -------------------------------------------------

#include "./script_wrappable_impl.h"

namespace blink {

static JSClassID g_scriptWrappableClassId = 0;
static const char ClassName[] = "ScriptWrappable";

ScriptWrappable::ScriptWrappable(void) = default;

ScriptWrappable::~ScriptWrappable(void)
{
    ASSERT(!IsBound());
}

void ScriptWrappable::Attach(JSRuntime *runtime)
{
    JSClassDef def = { 0 };
    def.class_name = ClassName;
    def.finalizer = ClassFinalizerCallback;

    static JSClassExoticMethods s_methods = GetClassExoticMethods();
    def.exotic = &s_methods;

    ASSERT(0 != g_scriptWrappableClassId);
    JS_NewClass(runtime, g_scriptWrappableClassId, &def);
}

JSValue ScriptWrappable::Bind(JSContext *ctx, JSValue scriptObject)
{
    ASSERT(!IsBound());

    m_ptr = JS_VALUE_GET_PTR(scriptObject);
    JS_SetOpaque(scriptObject, this);

    if (GCObject *o = ObjectForGC())
        o->IncRef();

    OnBind(ctx, scriptObject);
    return scriptObject;
}

ScriptWrappable* ScriptWrappable::CastImpl(JSValue v)
{
    return reinterpret_cast<ScriptWrappable *>(JS_GetOpaque(v, g_scriptWrappableClassId));
}

void ScriptWrappable::ClassFinalizerCallback(JSRuntime *rt, JSValue val)
{
    ScriptWrappable *p = ScriptWrappable::Cast<ScriptWrappable>(val);
    ASSERT(p->IsEqual(val));

    p->m_ptr = nullptr;
    p->OnFinalize();
    if (GCObject *o = p->ObjectForGC())
        o->Release();
}

JSValue ScriptWrappable::DupValue(JSContext *ctx) const
{
    ASSERT(nullptr != m_ptr);
    return JS_DupValue(ctx, JS_MKPTR(JS_TAG_OBJECT, m_ptr));
}

JSClassExoticMethods ScriptWrappable::GetClassExoticMethods(void)
{
    JSClassExoticMethods ret = { 0 };
    ret.get_own_property = GetOwnPropertyCallback;
#ifdef BLINKIT_UI_ENABLED
    ret.set_property = SetPropertyCallback;
#endif
    return ret;
}

int ScriptWrappable::GetOwnProperty(JSContext *ctx, JSPropertyDescriptor *desc, JSAtom prop)
{
    return 0; // Default behavior (false) - let QuickJS to go through prototypes.
}

int ScriptWrappable::GetOwnPropertyCallback(JSContext *ctx, JSPropertyDescriptor *desc, JSValueConst obj, JSAtom prop)
{
    ScriptWrappable *p = CastImpl(obj);
    if (nullptr == p)
    {
        ASSERT(nullptr != p);
        return -1;
    }

    ASSERT(p->IsEqual(obj));
    return p->GetOwnProperty(ctx, desc, prop);
}

void ScriptWrappable::Initialize(void)
{
    ASSERT(0 == g_scriptWrappableClassId);
    JS_NewClassID(&g_scriptWrappableClassId);
}

bool ScriptWrappable::IsEqual(JSValue v) const
{
    return JS_VALUE_GET_PTR(v) == m_ptr;
}

bool ScriptWrappable::MatchType(TypeId) const
{
    ASSERT_NOT_REACHED();
    return false;
}

JSValue ScriptWrappable::Wrap(JSContext *ctx)
{
    if (IsBound())
        return DupValue(ctx);

    JSValue object = JS_NewObjectClass(ctx, g_scriptWrappableClassId);
    return Bind(ctx, object);
}

JSValue ScriptWrappable::Wrap(JSContext *ctx, JSValue proto)
{
    ASSERT(!IsBound());
    JSValue ret = JS_NewObjectProtoClass(ctx, proto, g_scriptWrappableClassId);
    return Bind(ctx, ret);
}

#ifdef BLINKIT_UI_ENABLED
int ScriptWrappable::CallSetter(JSContext *ctx, JSValueConst obj, JSValue setter, JSAtom atom, JSValueConst value, int flags)
{
    int r = 1;
    JSValue retVal;

    if (JS_IsObject(setter))
    {
        setter = JS_DupValue(ctx, setter);

        retVal = JS_Call(ctx, setter, obj, 1, &value);
        if (JS_IsException(retVal))
            r = -1;
        JS_FreeValue(ctx, retVal);

        JS_FreeValue(ctx, setter);
    }
    else
    {
        ASSERT(false); // BKTODO:
        JS_FreeValue(ctx, value);
        r = -1;
    }
    return r;
}

int ScriptWrappable::SetProperty(JSContext *ctx, JSAtom atom, JSValueConst value, JSValueConst receiver, int flags)
{
    return 0; // Default behavior (false) - go through prototypes.
}

int ScriptWrappable::SetPropertyByDefault(JSContext *ctx, JSValueConst obj, JSAtom atom, JSValueConst value, int flags)
{
    JSPropertyDescriptor desc;

    JSValue proto = JS_GetPrototype(ctx, obj);
    ASSERT(JS_IsObject(proto));
    do {
        int r = JS_GetOwnProperty(ctx, &desc, proto, atom);
        if (0 != r)
        {
            if (1 == r)
            {
                if (JS_PROP_GETSET & desc.flags)
                    r = CallSetter(ctx, obj, desc.setter, atom, value, flags);
                else
                    ASSERT(false); // BKTODO:
                JS_FreeValue(ctx, desc.value);
                JS_FreeValue(ctx, desc.getter);
                JS_FreeValue(ctx, desc.setter);
            }
            JS_FreeValue(ctx, proto);
            return r;
        }

        JSValue proto2 = JS_GetPrototype(ctx, proto);
        JS_FreeValue(ctx, proto);
        proto = proto2;
    } while (JS_IsObject(proto));

    flags |= JS_PROP_HAS_ENUMERABLE | JS_PROP_HAS_WRITABLE | JS_PROP_HAS_CONFIGURABLE
        | JS_PROP_HAS_VALUE | JS_PROP_C_W_E
        | JS_PROP_NO_EXOTIC;
    return JS_DefineProperty(ctx, obj, atom, value, JS_UNDEFINED, JS_UNDEFINED, flags);
}

int ScriptWrappable::SetPropertyCallback(JSContext *ctx, JSValueConst obj, JSAtom atom, JSValueConst value, JSValueConst receiver, int flags)
{
    ScriptWrappable *p = CastImpl(obj);
    if (nullptr == p)
    {
        ASSERT(nullptr != p);
        return -1;
    }
    ASSERT(p->IsEqual(obj));

    int r = p->SetProperty(ctx, atom, value, receiver, flags);
    if (0 != r)
        return r;

    return SetPropertyByDefault(ctx, obj, atom, value, flags);
}
#endif // BLINKIT_UI_ENABLED

} // namespace blink
